﻿using Microsoft.EntityFrameworkCore;

namespace FlexField.Metadata.EntityFrameworkCore
{
    /// <summary>
    /// Represents a new instance of a persistence store for the fields.
    /// </summary>
    /// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
    /// <typeparam name="TKey">The type of the primary key for a field.</typeparam>
    public class DataFieldStore<TContext,TKey> : IFieldStore<TKey> 
        where TContext : DbContext 
        where TKey : IEquatable<TKey>
    {
        private bool _disposed;

        /// <summary>
        /// Constructs a new instance of <see cref="DataFieldStore{TContext,TKey}"/>.
        /// </summary>
        /// <param name="context">The <see cref="MetadataDbContext"/>.</param>
        /// <param name="keyNormalizer">The <see cref="ILookupNormalizer"/> to use when generating index keys for fields.</param>
        /// <param name="describer">The <see cref="MetadataErrorDescriber"/>.</param>
        public DataFieldStore(TContext context, ILookupNormalizer keyNormalizer = null, MetadataErrorDescriber describer = null)
        {
            Context = context ?? throw new ArgumentNullException(nameof(context));
            ErrorDescriber = describer ?? new MetadataErrorDescriber();
            KeyNormalizer = keyNormalizer;
        }

        /// <summary>
        /// Throws if this class has been disposed.
        /// </summary>
        protected void ThrowIfDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }

        /// <summary>
        /// Dispose the stores
        /// </summary>
        public void Dispose() => _disposed = true;

        /// <summary>
        /// Gets the database context for this store.
        /// </summary>
        public virtual TContext Context { get; }

        /// <summary>
        /// The <see cref="ILookupNormalizer"/> used to normalize things like field names.
        /// </summary>
        public ILookupNormalizer KeyNormalizer { get; set; }

        /// <summary>
        /// Gets or sets the <see cref="MetadataErrorDescriber"/> for any error that occurred with the current operation.
        /// </summary>
        public MetadataErrorDescriber ErrorDescriber { get; }

        /// <summary>
        /// A navigation property for the fields the store contains.
        /// </summary>
        public virtual IQueryable<DataField<TKey>> DataFields => Context.Set<DataField<TKey>>();

        private async Task<DataField<TKey>> FindDataFieldAsync(TKey fieldId, CancellationToken cancellationToken)
        {
            return await DataFields.Include(t => t.Properties)
                .SingleOrDefaultAsync(t => t.Id.Equals(fieldId), cancellationToken);
        }

        private MetadataField<TKey> ConvertToMetadata(DataField<TKey> field)
        {
            if (field == null) return null;

            var metadata = new MetadataField<TKey>
            {
                Id = field.Id,
                FieldName = field.FieldName,
                DisplayName = field.DisplayName,
                ValueType = field.ValueType,
                ConcurrencyStamp = field.ConcurrencyStamp
            };
            foreach (var property in field.Properties)
            {
                metadata.Properties[property.PropertyName] = property.PropertyValue;
            }

            return metadata;
        }

        /// <summary>
        /// Normalize field name for consistent comparisons.
        /// </summary>
        /// <param name="name">The name to normalize.</param>
        /// <returns>A normalized value representing the specified <paramref name="name"/>.</returns>
        protected virtual string NormalizeName(string name)
            => KeyNormalizer == null ? name : KeyNormalizer.NormalizeName(name);

        /// <inheritdoc />
        public virtual async Task<MetadataResult> CreateAsync(TKey entityId, MetadataField<TKey> field, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            await Context.AddAsync(new DataField<TKey>
            {
                EntityId = entityId,
                Id = field.Id,
                FieldName = field.FieldName,
                NormalizedFieldName = NormalizeName(field.FieldName),
                DisplayName = field.DisplayName,
                ValueType = field.ValueType,
                ConcurrencyStamp = field.ConcurrencyStamp
            }, cancellationToken);

            foreach (var (key, value) in field.Properties)
            {
                await Context.AddAsync(new DataFieldProperty<TKey>
                {
                    FieldId = field.Id,
                    PropertyName = key,
                    NormalizedPropertyName = NormalizeName(key),
                    PropertyValue = value
                }, cancellationToken);
            }

            await Context.SaveChangesAsync(cancellationToken);
            return MetadataResult.Success;
        }

        /// <inheritdoc />
        public virtual async Task<MetadataResult> UpdateAsync(TKey entityId, MetadataField<TKey> field, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            var dataField = await FindDataFieldAsync(field.Id, cancellationToken);
            if (dataField == null)
            {
                return MetadataResult.Failed(ErrorDescriber.FieldNotFound(field.Id.ToString(), entityId.ToString()));
            }

            dataField.EntityId = entityId;
            dataField.FieldName = field.FieldName;
            dataField.NormalizedFieldName = NormalizeName(field.FieldName);
            dataField.DisplayName = field.DisplayName;
            dataField.ValueType = field.ValueType;
            Context.Entry(dataField).OriginalValues[nameof(DataField<TKey>.ConcurrencyStamp)] = field.ConcurrencyStamp;
            dataField.ConcurrencyStamp = Guid.NewGuid().ToString();

            var properties = dataField.Properties.ToList();
            foreach (var (key, value) in field.Properties)
            {
                var normalizedPropertyName = NormalizeName(key);
                var dataProperty = properties.Find(property => property.NormalizedPropertyName == normalizedPropertyName);
                if (dataProperty == null)
                {
                    await Context.AddAsync(dataProperty = new DataFieldProperty<TKey>
                    {
                        FieldId = field.Id,
                        PropertyName = key,
                        NormalizedPropertyName = normalizedPropertyName
                    }, cancellationToken);
                }
                else
                {
                    properties.Remove(dataProperty);
                }
                dataProperty.PropertyValue = value;
            }

            Context.RemoveRange(properties);
            try
            {
                await Context.SaveChangesAsync(cancellationToken);
                field.ConcurrencyStamp = dataField.ConcurrencyStamp;
            }
            catch (DbUpdateConcurrencyException)
            {
                return MetadataResult.Failed(ErrorDescriber.ConcurrencyFailure());
            }

            return MetadataResult.Success;
        }

        /// <inheritdoc />
        public virtual async Task<MetadataResult> DeleteAsync(TKey entityId, MetadataField<TKey> field, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (field == null)
            {
                throw new ArgumentNullException(nameof(field));
            }

            var dataField = await FindDataFieldAsync(field.Id, cancellationToken);
            if (dataField == null)
            {
                return MetadataResult.Failed(ErrorDescriber.FieldNotFound(field.Id.ToString(), entityId.ToString()));
            }

            Context.RemoveRange(dataField.Properties);
            Context.Entry(dataField).OriginalValues[nameof(DataField<TKey>.ConcurrencyStamp)] = field.ConcurrencyStamp;
            dataField.ConcurrencyStamp = Guid.NewGuid().ToString();
            Context.Remove(dataField);

            try
            {
                await Context.SaveChangesAsync(cancellationToken);
            }
            catch (DbUpdateConcurrencyException)
            {
                return MetadataResult.Failed(ErrorDescriber.ConcurrencyFailure());
            }

            return MetadataResult.Success;
        }

        /// <inheritdoc />
        public virtual async Task<MetadataField<TKey>> FindByIdAsync(TKey fieldId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            return ConvertToMetadata(await FindDataFieldAsync(fieldId, cancellationToken));
        }

        /// <inheritdoc />
        public virtual async Task<MetadataField<TKey>> FindByNameAsync(TKey entityId, string fieldName, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            fieldName = NormalizeName(fieldName);
            return ConvertToMetadata(await DataFields.Include(field => field.Properties)
                .SingleOrDefaultAsync(field => field.NormalizedFieldName == fieldName && field.EntityId.Equals(entityId), cancellationToken));
        }

        /// <inheritdoc />
        public virtual async Task<IList<MetadataField<TKey>>> FindByPropertyAsync(TKey entityId, string propertyName, string propertyValue, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            propertyName = NormalizeName(propertyName);
            return (await DataFields.Include(field => field.Properties).OrderBy(field => field.DisplayName ?? field.FieldName)
                .Where(field => field.EntityId.Equals(entityId) && field.Properties.Any(p => p.NormalizedPropertyName == propertyName && p.PropertyValue == propertyValue))
                .ToArrayAsync(cancellationToken)).Select(ConvertToMetadata).ToArray();
        }

        /// <inheritdoc />
        public virtual async Task<IList<MetadataField<TKey>>> GetFieldsInTypeAsync(TKey entityId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            return (await DataFields.Include(field => field.Properties)
                .OrderBy(field => field.DisplayName ?? field.FieldName)
                .Where(field => field.EntityId.Equals(entityId))
                .ToArrayAsync(cancellationToken)).Select(ConvertToMetadata).ToArray();
        }
    }
}
